package org.jenkinsci.plugins.p4scm.utils;

import hudson.FilePath;
import hudson.model.Run;
import hudson.util.LogTaskListener;

import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;

import org.jenkinsci.plugins.p4scm.P4SCM;
import org.jenkinsci.plugins.p4scm.P4TagAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CommonUtil {
    
    private static final Logger logger = LoggerFactory.getLogger(CommonUtil.class);
    
    /* Regular expressions for parsing view mappings.
     */
    private static final Pattern COMMENT = Pattern.compile("^\\s*$|^#.*$");
    private static final Pattern DEPOT_ONLY = Pattern.compile("^\\s*[+-]?//\\S+?(/\\S+)\\s*$");
    private static final Pattern DEPOT_ONLY_QUOTED = Pattern.compile("^\\s*\"[+-]?//\\S+?(/[^\"]+)\"\\s*$");
    private static final Pattern DEPOT_AND_WORKSPACE =
            Pattern.compile("^\\s*([+-]?//\\S+?/\\S+)\\s+//\\S+?(/\\S+)\\s*$");
    private static final Pattern DEPOT_AND_WORKSPACE_QUOTED =
            Pattern.compile("^\\s*\"([+-]?//\\S+?/[^\"]+)\"\\s+\"//\\S+?(/[^\"]+)\"\\s*$");
    private static final Pattern DEPOT_AND_QUOTED_WORKSPACE =
            Pattern.compile("^\\s*([+-]?//\\S+?/\\S+)\\s+\"//\\S+?(/[^\"]+)\"\\s*$");
    private static final Pattern QUOTED_DEPOT_AND_WORKSPACE =
            Pattern.compile("^\\s*\"([+-]?//\\S+?/[^\"]+)\"\\s+//\\S+?(/\\S+)$\\s*");

    private CommonUtil() {}
    
    public static void listFilePath(FilePath fp) {
        listFilePath(fp, 0);
    }
    
    public static void listFilePath(FilePath fp, int level) {
        
        try {
            if(!fp.exists()) return;
            
            List<FilePath> subList = fp.list();
            if(subList == null ) return;
            
            for(FilePath sub : subList) {
                logger.debug("{}", sub);
                if(level>0){
                    listFilePath(sub, level-1);
                }
            }
            
        } catch (Exception e) {
            logger.error("List file path {} fail", fp, e);
        }
    }
    
    @SuppressWarnings("unchecked")
    public static void logRequest(HttpServletRequest req) {
        
        logger.debug("Request Parameters:");
        req.getParameterMap().keySet().forEach(key -> {
            logger.debug("Key: {}, Value: {}", key, req.getParameter(key.toString()));
        });
    }
    
    /**
     * Function for converting map of environment variables to a String
     * array as hudson does not provide a launcher method that takes
     * <p/>
     * (1) Environment Map (2) InputStream (3) OutputStream
     * <p> .. at the same time
     *
     * @param envMap
     *
     * @return
     */
    public static String[] convertEnvMaptoArray(Map<String, String> envMap) {
        Set<String> keySet = envMap.keySet();
        String[] keys = keySet.toArray(new String[0]);

        String[] result = new String[keys.length];
        for (int i = 0; i < keys.length; i++)
            result[i] = keys[i] + "=" + envMap.get(keys[i]);

        return result;
    }
    
    public static boolean doesFilenameMatchP4Pattern(String filename, String patternString, 
            boolean caseSensitive) throws PatternSyntaxException {
        patternString = patternString.trim();
        filename = filename.trim();
        patternString = patternString.replaceAll("\\*", "[^/]*");
        patternString = patternString.replaceAll("\\.\\.\\.", ".*");
        patternString = patternString.replaceAll("%%[0-9]", "[^/]*");
        patternString = patternString.replaceAll("^\"", "");
        patternString = patternString.replaceAll("\"$", "");
        filename = filename.replaceAll("^\"", "");
        filename = filename.replaceAll("\"$", "");
        Pattern pattern = Pattern.compile(patternString, !caseSensitive ? Pattern.CASE_INSENSITIVE : 0);
        Matcher matcher = pattern.matcher(filename);
        return matcher.matches();
    }
    
    /**
     * This takes a java.util.Date and converts it to a string.
     *
     * @return A string representation of the date
     */
    public static String javaDateToStringDate(java.util.Date newDate) {
        if (newDate == null)
            return "";

        GregorianCalendar cal = (GregorianCalendar) Calendar.getInstance();
        cal.clear();
        cal.setTime(newDate);

        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH) + 1;
        int day = cal.get(Calendar.DAY_OF_MONTH);

        int hour = cal.get(Calendar.HOUR_OF_DAY);
        int min = cal.get(Calendar.MINUTE);
        int sec = cal.get(Calendar.SECOND);

        String date = year + "-" + putZero(month) + "-" + putZero(day);
        if (hour + min + sec > 0)
            date += " " + putZero(hour) + ":" + putZero(min) + ":" + putZero(sec);

        return date;
    }
    
    public static String putZero(int i) {
        if (i < 10) {
            return "0" + i;
        }
        return i + "";
    }
    
    /**
     * Returns a java.util.Date object set to the time specified in newDate. The
     * format expected is the format of: YYYY-MM-DD HH:MM:SS
     *
     * @param newDate
     *            the string date to convert
     * @return A java.util.Date based off of the string format.
     */
    public static java.util.Date stringDateToJavaDate(String newDate) {
        // when we have a null from the database, give it zeros first.
        if (newDate == null || newDate.equals("")) {
            return null;
        }

        String[] parts = newDate.split(" ");
        String[] date = parts[0].split("-");
        String[] time = null;

        if (parts.length > 1) {
            time = parts[1].split(":");
            time[2] = time[2].replaceAll("\\.0", "");
        } else {
            time = "00:00:00".split(":");
        }

        GregorianCalendar cal = (GregorianCalendar) Calendar.getInstance();
        cal.clear();

        cal.set(new Integer(date[0]).intValue(), (new Integer(date[1]).intValue() - 1),
                new Integer(date[2]).intValue(), new Integer(time[0]).intValue(), new Integer(time[1]).intValue(),
                new Integer(time[2]).intValue());

        return cal.getTime();
    }
    
    @Nonnull
    public static String processPathName(@Nonnull String path, boolean isUnix) {
        String pathName = path;
        pathName = pathName.replaceAll("/\\./", "/");
        pathName = pathName.replaceAll("\\\\\\.\\\\", "\\\\");
        pathName = pathName.replaceAll("/+", "/");
        boolean isRemoteUNC = pathName.startsWith("\\\\");
        pathName = pathName.replaceAll("\\\\+", "\\\\");
        if (isRemoteUNC) {
            pathName = "\\" + pathName;
        }
        if (isUnix) {
            pathName = pathName.replaceAll("\\\\", "/");
        } else {
            pathName = pathName.replaceAll("/", "\\\\");
        }
        return pathName;
    }
    
    public static String escapeP4String(String string) {
        if (string == null) return null;
        String result = new String(string);
        result = result.replace("%","%25");
        result = result.replace("@","%40");
        result = result.replace("#","%23");
        result = result.replace("*","%2A");
        return result;
    }

    public static String unescapeP4String(String string) {
        if (string == null) return null;
        String result = new String(string);
        result = result.replace("%40","@");
        result = result.replace("%23","#");
        result = result.replace("%2A","*");
        result = result.replace("%25","%");
        return result;
    }
    
    
    /**
     * Perform some manipulation on the workspace URI to get a valid local path
     * <p>
     * Is there an issue doing this?  What about remote workspaces?  does that happen?
     *
     * @param path
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Nonnull
    public static String getLocalPathName(@Nonnull FilePath path, boolean isUnix) 
            throws IOException, InterruptedException {
        return processPathName(path.getRemote(), isUnix);
    }
    
    /**
     * Compares a parsed project path pair list against a list of view
     * mapping lines from a client spec.
     */
    public static boolean equalsProjectPath(List<String> pairs, List<String> lines) {
        Iterator<String> pi = pairs.iterator();
        for (String line : lines) {
            if (!pi.hasNext())
                return false;
            String p1 = pi.next();
            String p2 = pi.next();  // assuming an even number of pair items
            if (!line.trim().equals(p1.trim() + " " + p2.trim()))
                return false;
        }
        return !pi.hasNext(); // equals iff there are no more pairs
    }
    
    public static int getLastChangeNoFirstChange(@CheckForNull Run<?,?> build) {

        // If we can't find a PerforceTagAction, we will default to 0.

        P4TagAction action = getMostRecentTagAction(build);
        if (action == null)
            return 0;

        //log.println("Found last change: " + action.getChangeNumber());
        return action.getChangeNumber();
    }
    
    @CheckForNull
    private static P4TagAction getMostRecentTagAction(@CheckForNull Run<?,?> build) {
        if (build == null)
            return null;

        P4TagAction action = build.getAction(P4TagAction.class);
        if (action != null)
            return action;

        // if build had no actions, keep going back until we find one that does.
        return getMostRecentTagAction(build.getPreviousBuild());
    }
    
    public static boolean doesFilenameMatchAnyP4Pattern(String filename, 
            @Nonnull List<String> patternStrings, boolean caseSensitive) {
        for (String patternString : patternStrings) {
            if (patternString.trim().equals("")) continue;
            if (doesFilenameMatchP4Pattern(filename, patternString, caseSensitive)) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Parses the projectPath into a list of pairs of strings representing the depot and client
     * paths. Even items are depot and odd items are client.
     * <p>
     * This parser can handle quoted or non-quoted mappings, normal two-part mappings, or one-part
     * mappings with an implied right part. It can also deal with +// or -// mapping forms.
     */
    public static List<String> parseProjectPath(String projectPath, String p4Client) {
        PrintStream log = (new LogTaskListener(java.util.logging.Logger.getLogger(P4SCM.class.getName()), 
                java.util.logging.Level.WARNING)).getLogger();
        return parseProjectPath(projectPath, p4Client, log);
    }
    
    public static List<String> parseProjectPath(String projectPath, String p4Client, PrintStream log) {
        List<String> parsed = new ArrayList<String>();
        for (String line : projectPath.split("\n")) {
            Matcher depotOnly = DEPOT_ONLY.matcher(line);
            if (depotOnly.find()) {
                // add the trimmed depot path, plus a manufactured client path
                parsed.add(line.trim());
                parsed.add("//" + p4Client + depotOnly.group(1));
            } else {
                Matcher depotOnlyQuoted = DEPOT_ONLY_QUOTED.matcher(line);
                if (depotOnlyQuoted.find()) {
                    // add the trimmed quoted depot path, plus a manufactured quoted client path
                    parsed.add(line.trim());
                    parsed.add("\"//" + p4Client + depotOnlyQuoted.group(1) + "\"");
                } else {
                    Matcher depotAndWorkspace = DEPOT_AND_WORKSPACE.matcher(line);
                    if (depotAndWorkspace.find()) {
                        // add the found depot path and the clientname-tweaked client path
                        parsed.add(depotAndWorkspace.group(1));
                        parsed.add("//" + p4Client + depotAndWorkspace.group(2));
                    } else {
                        Matcher depotAndWorkspaceQuoted = DEPOT_AND_WORKSPACE_QUOTED.matcher(line);
                        if (depotAndWorkspaceQuoted.find()) {
                           // add the found depot path and the clientname-tweaked client path
                            parsed.add("\"" + depotAndWorkspaceQuoted.group(1) + "\"");
                            parsed.add("\"//" + p4Client + depotAndWorkspaceQuoted.group(2) + "\"");
                        } else {
                            Matcher depotAndQuotedWorkspace = DEPOT_AND_QUOTED_WORKSPACE.matcher(line);
                            if (depotAndQuotedWorkspace.find()) {
                                // add the found depot path and the clientname-tweaked client path
                                parsed.add(depotAndQuotedWorkspace.group(1));
                                parsed.add("\"//" + p4Client + depotAndQuotedWorkspace.group(2) + "\"");
                            } else {
                                Matcher quotedDepotAndWorkspace = QUOTED_DEPOT_AND_WORKSPACE.matcher(line);
                                if (quotedDepotAndWorkspace.find()) {
                                    parsed.add("\"" + quotedDepotAndWorkspace.group(1) + "\"");
                                    parsed.add("//" + p4Client + quotedDepotAndWorkspace.group(2));
                                } else {
                                    // Assume anything else is a comment and ignore it
                                    if (line.trim().length() > 0 && !line.startsWith("#")) {
                                        // Throw a warning only if the line is not blank and not '#' prefixed.
                                        log.println("Warning: Client Spec line invalid, ignoring. ("+line+")");
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return parsed;
    }
    
    public static boolean isFileInView(String filename, String projectPath, boolean caseSensitive) {
        List<String> view = parseProjectPath(projectPath, "workspace");
        boolean inView = false;
        for (int i = 0; i < view.size(); i += 2) {
            String viewline = view.get(i);
            if (viewline.startsWith("-")) {
                if (doesFilenameMatchP4Pattern(filename, viewline.substring(1), caseSensitive)) {
                    inView = false;
                }
            } else if (viewline.startsWith("+")) {
                if (doesFilenameMatchP4Pattern(filename, viewline.substring(1), caseSensitive)) {
                    inView = true;
                }
            } else {
                if (doesFilenameMatchP4Pattern(filename, viewline, caseSensitive)) {
                    inView = true;
                }
            }
        }
        return inView;
    }
    
    public static boolean isValidClientSpec(String clientspec) {
       return isValidSpec(clientspec);
    }
    
    public static boolean isValidFileSpec(String file) {
        return isValidSpec(file);
    }
    
    public static boolean isValidSpec(String spec) {
        return DEPOT_ONLY.matcher(spec).matches() || DEPOT_ONLY_QUOTED.matcher(spec).matches();
    }
    
}
